import requests
from bs4 import BeautifulSoup
import re
import time
import os
import sys
from urllib.parse import urljoin
import json
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

# 尝试初始化OpenAI客户端，如果失败则设置为None
client = None
try:
    # 只导入必要的库
    from openai import OpenAI
    # 只传递最基本的参数
    client = OpenAI(
        api_key=os.getenv('DEEPSEEK_API_KEY', 'your_api_key_here'),
        base_url=os.getenv('DEEPSEEK_BASE_URL', 'https://api.deepseek.com/v1')
    )
    print("DeepSeek客户端初始化成功")
except Exception as e:
    print(f"OpenAI客户端初始化失败: {str(e)}")
    print("将跳过大模型文本总结功能")


def clean_text_with_llm(text):
    """使用大模型总结和清理文本内容，如果大模型不可用则使用基本的文本清理"""
    # 先进行基本的文本清理
    cleaned_text = re.sub(r'\n+', '\n', text).strip()  # 替换连续换行符为单个并去除首尾空白
    
    # 清理控制字符（如\u0001 \u0002等）
    cleaned_text = re.sub(r'[\x00-\x1f\x7f]', ' ', cleaned_text)  # 移除ASCII控制字符
    cleaned_text = re.sub(r'\s+', ' ', cleaned_text)  # 替换多余空格
    
    # 进一步优化：替换孤立的换行符为空格，但保留段落间的换行
    # 处理数字与文本间的换行问题
    cleaned_text = re.sub(r'(\d+)\s*\n\s*(\D)', '\1 \2', cleaned_text)
    cleaned_text = re.sub(r'(\D)\s*\n\s*(\d+)', '\1 \2', cleaned_text)
    
    # 如果大模型客户端可用，则使用大模型进行总结
    if client is not None:
        try:
            response = client.chat.completions.create(
                model=os.getenv('DEEPSEEK_MODEL', 'deepseek-chat'),
                messages=[
                    {
                        "role": "system",
                        "content": "你是一个文本处理助手，负责总结文本内容并清理多余的换行符。请先理解文本的主要内容，然后提供简洁的总结，同时保留关键信息。特别注意清理数字与文本之间的换行，确保输出内容中没有多余的换行符。"
                    },
                    {
                        "role": "user",
                        "content": f"请总结以下竞赛信息，并清理所有多余的换行符，尤其是数字与文本之间的换行：\n{text}"
                    }
                ],
                temperature=0.3,
            )
            return response.choices[0].message.content.strip()
        except Exception as e:
            print(f"调用大模型API失败: {str(e)}")
            # 失败时返回基本清理后的文本
            return cleaned_text
    
    # 如果大模型不可用，返回基本清理后的文本
    return cleaned_text

# 加载环境变量
load_dotenv()

class CompetitionScraper:
    def __init__(self, base_url, headers=None, delay=0.1):
        self.session = requests.Session()
        self.base_url = base_url
        # 增强的请求头信息，模拟真实浏览器
        self.headers = headers or {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Accept-Encoding': 'gzip, deflate, br',
            'Referer': 'https://www.bkjx.sdu.edu.cn/',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
            'Cache-Control': 'max-age=0',
            'TE': 'Trailers'
        }
        self.delay = delay  # 爬取延迟，避免请求过于频繁
        self.competitions = []
        
        # 确保session使用统一的cookie jar
        self.session.headers.update(self.headers)

    def fetch_page(self, url):
        """获取网页内容，并确保正确处理编码和检查访问权限"""
        try:
            response = self.session.get(url, headers=self.headers, timeout=15)
            response.raise_for_status()  # 检查请求是否成功
            
            # 自动检测并使用正确编码解码内容
            encoding = response.apparent_encoding
            content = response.content.decode(encoding, errors='replace')
            
            # 检查页面是否包含系统提示或无权限访问信息
            if "系统提示" in content or "没有访问权限" in content or "无权限" in content:
                print(f"警告: 页面 {url} 返回系统提示或无权限访问信息")
                # 保存错误页面内容以便分析
                error_dir = 'error_pages'
                os.makedirs(error_dir, exist_ok=True)
                error_file = os.path.join(error_dir, f'error_{int(time.time())}.html')
                with open(error_file, 'w', encoding='utf-8') as f:
                    f.write(content)
                print(f"错误页面已保存到: {error_file}")
            
            print(f"成功获取页面: {url}")
            return content
        except requests.exceptions.RequestException as e:
            print(f"获取页面失败: {e}")
            return None

    def fetch_competition_details(self, url):
        """获取竞赛详情页内容"""
        try:
            print(f"[调试] 正在获取竞赛详情: {url}")
            response = self.session.get(url, timeout=15)
            print(f"[调试] 响应状态码: {response.status_code}")
            response.raise_for_status()
            
            # 自动检测并使用正确编码解码内容
            encoding = response.apparent_encoding
            html_content = response.content.decode(encoding, errors='replace')
            soup = BeautifulSoup(html_content, 'html.parser')

            # 尝试多种选择器提取内容
            content_selectors = [
                'div#content', 'div.v_news_content', 'div.article', 
                'div.leftNews3', 'div.main-content', 'div.news_content'
            ]
            content_div = None
            for selector in content_selectors:
                content_div = soup.select_one(selector)
                if content_div:
                    print(f"[调试] 找到内容区域: {selector}")
                    break

            if content_div:
                # 提取所有文本内容（不只限于p标签）
                text_parts = [text.strip() for text in content_div.stripped_strings if text.strip()]
                # 合并文本并初步清理多余换行符
                description = '\n'.join(text_parts[:50])  # 限制长度避免过长
                description = re.sub(r'\n+', '\n', description).strip()  # 替换连续换行符为单个并去除首尾空白

                # 不需要在这里调用大模型API，后面会统一处理
                print(f"[调试] 提取到描述长度: {len(description)}字符")
                return description
            else:
                print(f"[调试] 所有选择器均未匹配到内容")
                print(f"无法在详情页找到内容区域: {url}")
                return '无法获取详细描述'
        except requests.exceptions.RequestException as e:
            error_msg = f'网络请求错误: {str(e)}'
            print(f"[错误] {error_msg}")
            print(error_msg)
            return error_msg
        except Exception as e:
            error_msg = f'处理详情页失败: {str(type(e))} - {str(e)}'
            print(f"[错误] {error_msg}")
            print(error_msg)
            return error_msg

    def parse_page(self, html):
        """解析页面内容，提取竞赛信息"""
        competitions = []  # 初始化竞赛信息列表
        soup = BeautifulSoup(html, 'html.parser')  # 使用内置解析器避免依赖问题
        
        # 打印页面标题进行调试
        page_title = soup.title.get_text(strip=True) if soup.title else '无标题'
        print(f"[调试] 页面标题: {page_title}")
        
        # 尝试多种可能的竞赛列表容器选择器
        # 优先使用之前成功过的选择器
        possible_selectors = [
            'div.leftNews3',  # 竞赛信息专用容器 - 之前成功使用过的选择器
            'div.news_list > li',  # 新闻列表项
            'div#content li',  # 内容区域所有列表项
            'div.content > ul > li',  # 内容区域列表
            'ul > li',  # 所有无序列表项
            'li'  # 任何列表项
        ]
        
        matched_selector = None
        items = []
        # 先尝试所有可能的选择器并记录结果
        selector_results = {}
        for selector in possible_selectors:
            found = soup.select(selector)
            selector_results[selector] = len(found)
            if found and not matched_selector:
                items = found
                matched_selector = selector
        
        # 打印所有选择器的匹配结果
        print("[调试] 选择器匹配结果:")
        for selector, count in selector_results.items():
            print(f"  {selector}: {count}个匹配项")
        
        if matched_selector:
            print(f"使用选择器'{matched_selector}'找到{len(items)}个竞赛条目")
        else:
            print("未找到竞赛信息容器，尝试匹配所有带链接的列表项")
            items = soup.select('li:has(a)')  # 最后尝试:所有包含链接的列表项
            print(f"  li:has(a): {len(items)}个匹配项")
        for item in items:
            try:
                # 提取竞赛标题
                # 提取竞赛标题（山东大学网站使用class为'title'的a标签）
                # 修正标题选择器
                # 使用更具体的标题选择器
                # 提取a标签文本并添加调试日志
                # 从tr中查找a标签获取标题
                title_tag = item.select_one('a')  # 更通用的链接选择器
                if not title_tag:
                        continue
                # 获取完整标题文本（包含省略号内容）
                title = title_tag.get('title', '').strip() or title_tag.get_text(strip=True)
                date_tag = item.select_one('div[style*="float:right"]')
                date = date_tag.get_text(strip=True) if date_tag else '未知日期'
                # 提取日期（去除[]括号）
                date = date.strip('[]')
                
                # 暂时移除关键词过滤，以确保能够抓取到信息
                # 后续可以根据实际情况调整或恢复关键词过滤
                # 对于竞赛相关条目，继续提取链接和详情
                
                # 提取竞赛链接
                link = title_tag['href'] if 'href' in title_tag.attrs else ''
                
                # 如果是相对链接，转换为绝对链接
                link = urljoin(self.base_url, link)
                
                # 获取竞赛详情
                description = self.fetch_competition_details(link)
                
                # 生成content字段 - 使用大模型总结文本内容
                # 如果文本过长，先进行初步清理再传入大模型
                if len(description) > 500:
                    # 对长文本进行预处理，但不粗暴截断
                    # 保留完整的句子结构
                    sentences = description.split('.')
                    # 取前一半句子（保留关键信息）
                    processed_text = '.'.join(sentences[:len(sentences)//2]) + ('.' if len(sentences) > 0 else '')
                    content = clean_text_with_llm(processed_text)
                else:
                    # 直接将完整文本传给大模型总结
                    content = clean_text_with_llm(description)
                
                # 减少延迟时间，避免请求间隔过长
                time.sleep(max(0.5, self.delay/2))  # 使用原始延迟的一半或至少0.5秒
                
                # 添加竞赛信息到结果列表，包含id字段，只保留content不包含description
                competitions.append({
                    'id': len(competitions) + 1,
                    'title': title,
                    'date': date,
                    'url': link,
                    'content': content
                })
                print(f"成功解析竞赛: {title}")
            except Exception as e:
                print(f"处理竞赛条目时出错: {str(e)}")
                continue


        return competitions

    def scrape(self, start_page=1, end_page=None):
        """执行爬取任务"""
        current_page = start_page
        seen_urls = set()  # 用于跟踪已爬取的竞赛链接，避免重复
        max_empty_pages = 2  # 最多允许连续多少个空页
        consecutive_empty_pages = 0  # 当前连续空页数
        
        while True:
            # 构建分页URL（根据实际网站分页规则修改）
            if end_page and current_page > end_page:
                break

            # 山东大学网站分页参数为'pager.offset'，每页显示10条
            page_url = f'{self.base_url}&pager.offset={(current_page-1)*10}' if current_page > 1 else self.base_url
            print(f"请求URL: {page_url}")
            html = self.fetch_page(page_url)

            if not html:
                print(f"无法获取第{current_page}页HTML内容，中断爬取")
                break

            parsed_competitions = self.parse_page(html)
            
            # 去重处理
            unique_competitions = []
            for competition in parsed_competitions:
                if competition['url'] not in seen_urls:
                    seen_urls.add(competition['url'])
                    unique_competitions.append(competition)
            
            # 如果本页没有新内容，增加连续空页数
            if len(unique_competitions) == 0:
                consecutive_empty_pages += 1
                print(f"第{current_page}页没有新内容，连续空页数: {consecutive_empty_pages}")
            else:
                consecutive_empty_pages = 0  # 重置连续空页数
                self.competitions.extend(unique_competitions)
                print(f"成功爬取第{current_page}页，新增{len(unique_competitions)}条竞赛信息")
            
            # 如果连续多个页面没有新内容，认为已经爬完所有信息
            if consecutive_empty_pages >= max_empty_pages:
                print(f"连续{max_empty_pages}个页面没有新内容，结束爬取")
                break
            
            new_count = len(parsed_competitions)

            # 检查是否有下一页（根据实际网站结构修改）
            # 这里使用简单的页数递增方式，实际可能需要检查'下一页'按钮
            current_page += 1
            time.sleep(self.delay)  # 设置爬取间隔

    def save_to_txt(self):
        """将竞赛信息保存为TXT文件"""
        if not self.competitions:
            print("没有爬取到竞赛信息，不保存文件")
            return

        # 创建存储目录
        save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'competition_details')
        os.makedirs(save_dir, exist_ok=True)
        print(f"竞赛详情存储目录: {save_dir}")

        for idx, competition in enumerate(self.competitions, 1):
            # 生成安全的文件名
            safe_title = re.sub(r'[\\/*?:"<>|]', '_', competition['title'])
            filename = f"{idx}_{safe_title[:50]}.txt"
            filepath = os.path.join(save_dir, filename)

            try:
                  # 使用UTF-8-BOM编码确保中文正常显示
                  with open(filepath, 'w', encoding='utf-8-sig') as f:
                      f.write(f"竞赛标题: {competition['title']}\n")
                      f.write(f"发布日期: {competition['date']}\n")
                      f.write(f"竞赛链接: {competition['url']}\n")
                      f.write(f"竞赛内容: {competition['content']}\n")
                      print(f"成功保存竞赛信息到: {filename}")
            except Exception as e:
                print(f"保存竞赛信息到TXT失败: {e}")

        return save_dir

    def save_to_json(self):
        """将竞赛信息保存为JSON格式文件"""
        # 创建存储目录
        save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'competition_details')
        os.makedirs(save_dir, exist_ok=True)
        json_filepath = os.path.join(save_dir, 'inform02.json')
        
        # 确保即使没有爬取到竞赛信息，也会创建JSON文件
        competitions_to_save = self.competitions or []

        try:
            with open(json_filepath, 'w', encoding='utf-8') as f:
                json.dump(competitions_to_save, f, ensure_ascii=False, indent=2)
            if competitions_to_save:
                print(f"成功保存{len(competitions_to_save)}条竞赛信息到JSON文件: {json_filepath}")
            else:
                print(f"创建了空的JSON文件: {json_filepath}")
            return json_filepath
        except Exception as e:
            print(f"保存竞赛信息到JSON失败: {e}")
            return None

def check_vpn_connection():
    """检查是否可能需要VPN连接"""
    print("\n=== 连接状态检查 ===")
    print("如果您在校外网络环境下，请确保已连接学校VPN。")
    
    # 尝试访问学校主页来验证基本连接
    try:
        import requests
        test_url = "https://www.sdu.edu.cn/"
        response = requests.get(test_url, timeout=10)
        print(f"学校主页访问状态: {response.status_code == 200}")
        print(f"响应状态码: {response.status_code}")
    except Exception as e:
        print(f"学校主页访问测试失败: {str(e)}")
        print("这可能表明您的网络环境需要VPN连接。")
    print("======================\n")

if __name__ == '__main__':
    # 记录程序开始时间
    start_time = time.time()
    print("程序启动")
    print(f"Python路径: {os.path.abspath(__file__)}")
    print(f"工作目录: {os.getcwd()}")
    
    # 检查VPN连接状态
    check_vpn_connection()
    
    scraper = None  # 初始化scraper变量
    try:
        # 从环境变量获取基础URL，或直接设置
        # 设置山东大学竞赛信息网站URL
        BASE_URL = os.getenv('COMPETITION_URL', 'https://www.bkjx.sdu.edu.cn/sanji_list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1266')
        print(f"使用的基础URL: {BASE_URL}")
        
        print("创建CompetitionScraper实例")
        scraper = CompetitionScraper(BASE_URL, delay=1)  # 降低延迟至1秒，平衡效率和反爬
        if not scraper:
            print("CompetitionScraper创建失败")
            import sys
            sys.exit(1)
        
        # 执行爬取（可以指定起始页和结束页）
        print("开始执行爬取操作")
        scraper.scrape(start_page=1, end_page=5)  # 增加爬取页面数量到5页以获取更多信息
        print(f"爬取完成，返回{len(scraper.competitions)}条竞赛信息")
        
        # 保存结果 - 无论是否有竞赛信息，都会创建JSON文件
        json_path = scraper.save_to_json()
        if scraper.competitions:
            print(f"爬取完成，结果已保存到JSON文件: {json_path}")
        else:
            print("爬取失败，未获取到任何竞赛信息，但已创建空JSON文件")
            print("\n可能的解决方案：")
            print("1. 确保您已连接学校VPN（如果在校外网络环境）")
            print("2. 检查校园网连接状态")
            print("3. 网站可能更新了反爬虫机制或访问控制策略")
            print("4. 尝试使用浏览器直接访问网站，确认能否正常查看竞赛信息")
    except Exception as e:
        print(f"程序执行出错: {str(e)}")
        print(f"错误类型: {type(e).__name__}")
        import traceback
        print("错误堆栈:")
        traceback.print_exc()
        print("\n可能的解决方案：")
        print("1. 确保您已连接学校VPN（如果在校外网络环境）")
        print("2. 检查网络连接状态")
        print("3. 确认网站URL是否正确")
    finally:
        # 计算程序运行时间
        end_time = time.time()
        runtime = end_time - start_time
        print(f"程序总运行时间: {runtime:.2f}秒")
        print("爬取任务完成")
        print("程序执行结束")
        if scraper:
            pass  # 空语句处理